﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using PerCederberg.Grammatica.Runtime;
using SafeScript.MetaModel;
using SafeScript.Debugger;

namespace SafeScript
{
    public class Compiler
    {
        [ThreadStatic]
        public static Compiler CurrentCompiler;

        private DebugFileGenerator debugFileGenerator;
        private Namespace globalNamespace;
        private ErrorReport report = new ErrorReport();
        private string[] lines;
        private Dictionary<string, string[]> linesByFile = new Dictionary<string, string[]>();
        Stack<string> filesStack;
        List<string> processedFiles;
        private List<TemplateInstanceTypeDef> UnfinishedTemplates = new List<TemplateInstanceTypeDef>();
        private List<TemplateInstanceTypeDef> Templates = new List<TemplateInstanceTypeDef>();
        private bool parseTemplatesImmediately = false;

        public List<string> ImportPaths = new List<string>();

        public Compiler()
        {
            CurrentCompiler = this;

            // TODO: better output name
            this.debugFileGenerator = new DebugFileGenerator(this, "out.htm");

            this.globalNamespace = new Namespace();
            // Define the builtin types
            this.globalNamespace.ScopeElements.Add("object", ObjectType.Self);
            this.globalNamespace.ScopeElements.Add("string", StringType.Self);
            this.globalNamespace.ScopeElements.Add("num", NumericType.Self);
            this.globalNamespace.ScopeElements.Add("bool", BoolType.Self);
            this.globalNamespace.ScopeElements.Add("void", VoidType.Self);
            this.globalNamespace.ScopeElements.Add("list#1", ListType.Self);
            this.globalNamespace.ScopeElements.Add("dict#2", DictType.Self);
            this.globalNamespace.ScopeElements.Add("null", NullType.Self);
        }

        public void ParseFiles(string[] filenames)
        {
            this.filesStack = new Stack<string>(filenames);
            this.processedFiles = new List<string>();

            while( filesStack.Count > 0 )
            {
                string filename = filesStack.Pop();
                this.processedFiles.Add(filename);

                string fullFilename = null;
                // Try to locate file
                foreach (string path in this.ImportPaths)
                {
                    if (path.Length == 0)
                        continue;
                    string p = path;
                    if (p[p.Length - 1] != '\\')
                        p = p + "\\";
                    if (File.Exists(p + filename))
                    {
                        fullFilename = p + filename;
                        break;
                    }
                }

                if (fullFilename == null)
                {
                    this.report.Add(new ErrorEntry(filename));
                    return;
                }

                try
                {
                    // Read the file from disk and parse it
                    using (StreamReader reader = new StreamReader(fullFilename))
                    {
                        string text = reader.ReadToEnd();
                        this.lines = text.Split('\n');
                        this.linesByFile[filename] = this.lines;
                        reader.Close();

                        StringReader sreader = new StringReader(text);
                        SafeScriptParser parser = new SafeScriptParser(sreader);
                        Node node = null;
                        try
                        {
                            node = parser.Parse();
                            node.Values.Add(filename);
                        }
                        catch (ParserLogException e)
                        {
                            for (int i = 0; i < e.GetErrorCount(); ++i)
                            {
                                ParseException pe = e.GetError(i);
                                this.report.Add(new ErrorEntry(pe, filename));
                            }
                            return;
                        }

                        // Parse the namespaces and types
                        this.globalNamespace.ParseMain(node, this.report, null);
                    }
                }
                catch (IOException ex)
                {
                    this.report.Add(new ErrorEntry(ex, filename));
                    return;
                }

                if (this.report.Count > 0)
                    return;
            }
        }

        internal void AddFile(string filename)
        {
            if (!this.filesStack.Contains(filename) && !this.processedFiles.Contains(filename))
                this.filesStack.Push(filename);
        }

        internal string GetFilename(Node node)
        {
            while (node.Parent != null)
                node = node.Parent;
            return node.Values[0] as string;
        }

        internal string GetText(string filename, int startLine, int startCol, int endLine, int endCol)
        {
            string[] lines = this.GetLines(filename);

            if (startLine == endLine)
                return lines[startLine-1].Substring(startCol - 1, endCol - startCol);
            string result = lines[startLine-1].Substring(startCol - 1);
            for (int i = startCol + 1; i < endCol; ++i)
                result += lines[i-1];
            result += lines[endLine-1].Substring(0, endCol - 1);
            return result;
        }

        internal string[] GetLines(string filename)
        {
            return this.linesByFile[filename];
        }

        public void Compile()
        {
            // Resolve names of imported namespaces and names of base classes/interfaces
            this.globalNamespace.ResolveMain(this.report);
            // Check the correctness of the inheritance hierarchy (especially check for loops)
            this.globalNamespace.ParseInheritanceAndMembers(this.report);
            // Parse and resolve all class members of templates
            this.ParseTemplates();
            // globalNamespace.ParseClassMembers(report);

            // Parse the initialization of member variables
            this.globalNamespace.ParseMemberVariableInitializtion(this.report);
            // Compute the type of member variables
            this.globalNamespace.ComputeMemberVariableTypes(this.report);
            // Parse the statements
            this.globalNamespace.ParseStatements(this.report);
        }

        public void Generate(string filename, string componentName, Generator.OutputType outputType)
        {
            Generator.JavaScriptGenerator gen = new SafeScript.Generator.JavaScriptGenerator(filename, componentName, outputType);
            gen.Generate(globalNamespace, report);
            gen.Close();

            this.debugFileGenerator.Parse(this.globalNamespace, gen);
            // Generate a debug file for this source code
            this.debugFileGenerator.Generate(this.report);
        }

        public void PrintErrors()
        {
            this.report.SortByLine();

            Console.WriteLine("{0} Errors", this.report.Count);
            foreach (ErrorEntry e in this.report)
            {
                Console.WriteLine("{0}", e.ToString());
            }
        }

        public ErrorReport ErrorReport
        {
            get
            {
                return this.report;
            }
        }

        /// <summary>
        /// Searches a type in the global namespace.
        /// </summary>
        /// <param name="scopedName"></param>
        /// <returns></returns>
        internal ITypeDef LookupType(string scopedName)
        {
            ScopedIdentifier ident = new ScopedIdentifier(scopedName.Split('.'));
            ScopeResolution res = this.globalNamespace.GetScopeElement(ident);
            if (res == null)
                return null;
            return res.GetFinalElement() as ITypeDef;
        }

        private void ParseTemplates()
        {
            this.parseTemplatesImmediately = true;

            while (this.UnfinishedTemplates.Count > 0)
            {
                TemplateInstanceTypeDef t = this.UnfinishedTemplates[0];
                this.UnfinishedTemplates.RemoveAt(0);
                t.ParseInheritanceAndMembers(report);
                this.Templates.Add(t);
            }
        }

        /// <summary>
        /// Searches through a list of already instantiated templates and returns it if found.
        /// Otherwise the template is instantiated, cached, and returned.
        /// </summary>
        /// <param name="template">Template to instantiate</param>
        /// <param name="templateArguments"></param>
        /// <returns></returns>
        internal TemplateInstanceTypeDef GetTemplateInstance(AbstractType template, Dictionary<TemplateParameterType, ITypeDef> templateArguments)
        {
            // Did we handle this template already?
            TemplateInstanceTypeDef t = this.GetTemplateInstance(this.Templates, template, templateArguments);
            if (t != null)
                return t;
            t = this.GetTemplateInstance(this.UnfinishedTemplates, template, templateArguments);
            if (t != null)
                return t;

            // Create the template
            TemplateInstanceTypeDef n = new TemplateInstanceTypeDef();
            n.Name = template.Name;
            n.Template = template;
            n.TemplateArguments = new Dictionary<TemplateParameterType,ITypeDef>(templateArguments);

            // It is possible that some template parameters are not covered by templateArguments.
            // These template parameters are handed down to the TemplateInstanceTypeDef.
            foreach (TemplateParameterType origTP in template.TemplateParameters)
                if (!templateArguments.ContainsKey(origTP))
                    n.TemplateParameters.Add(origTP);

            // Instantiate base classes if they are templates
            foreach (AbstractType b in template.BaseTypes)
            {
                // Only templates which depend on this instantiation must be instantiated.
                // Every other base class can be used as before
                if (b is TemplateInstanceTypeDef)
                {
                    bool required = false;
                    // Get the template base type
                    TemplateInstanceTypeDef basetemplate = b as TemplateInstanceTypeDef;
                    // Perhaps we have to modify the template arguments of the base type
                    Dictionary<TemplateParameterType,ITypeDef> bargs = new Dictionary<TemplateParameterType,ITypeDef>();
                    // Does the template instantiation depend on the template parameters of our template?
                    foreach (TemplateParameterType tp in basetemplate.TemplateArguments.Keys)
                    {
                        ITypeDef ta = basetemplate.TemplateArguments[tp];
                        if (ta is TemplateParameterType && templateArguments.ContainsKey(ta as TemplateParameterType))
                        {
                            required = true;
                            bargs[tp] = templateArguments[ta as TemplateParameterType];
                        }
                        else
                            bargs[tp] = ta;
                    }
                    if (required)
                        n.BaseTypes.Add(this.GetTemplateInstance(basetemplate.Template, bargs));
                    else
                        n.BaseTypes.Add(b);
                }
                else
                    n.BaseTypes.Add(b);
            }

            // Instantiate all nested classes and interfaces
            foreach (object obj in template.ScopeElements.Values)
            {
                if (obj is AbstractType)
                {
                    AbstractType at = obj as AbstractType;
                    // TODO: templateArguments is wrong here
                    n.ScopeElements[at.Name] = this.GetTemplateInstance(at, templateArguments);
                }
            }

            if (this.parseTemplatesImmediately)
            {
                n.ParseInheritanceAndMembers(this.report);
                this.Templates.Add(n);
            }
            else
                this.UnfinishedTemplates.Add(n);
            return n;
        }

        /// <summary>
        /// Searches through a list of already instantiated templates
        /// </summary>
        /// <param name="templates">List of already instantiated templates</param>
        /// <param name="template">Template to instantiate</param>
        /// <param name="templateArguments"></param>
        /// <returns>Returns null if not found</returns>
        private TemplateInstanceTypeDef GetTemplateInstance(List<TemplateInstanceTypeDef> templates, AbstractType template, Dictionary<TemplateParameterType, ITypeDef> templateArguments)
        {
            // Did we handle this template already?
            foreach (TemplateInstanceTypeDef t in templates)
            {
                if (!(t.Template.IsEqualTo(template)))
                    continue;
                if (t.TemplateArguments.Count != templateArguments.Count)
                    continue;
                bool ok = true;
                foreach (TemplateParameterType tp in t.TemplateArguments.Keys)
                {
                    if (!templateArguments.ContainsKey(tp))
                    {
                        ok = false;
                        break;
                    }
                    if (!t.TemplateArguments[tp].IsEqualTo(templateArguments[tp]))
                    {
                        ok = false;
                        break;
                    }
                }

                if (ok)
                    return t;
            }
            return null;
        }
    }
}
